'''
Created on 31/08/2009

@author: eh01
'''
from M2Crypto import SSL, httpslib
from datastore.database import DataStore
from datastore.resource import Resource, ResourceDict
from runtime.task import Task
from urlparse import urlparse
from xml.etree.ElementTree import ElementTree, Element
from xml.etree import ElementTree as ET
import logging
import httplib
import os
import types

#################################################################
# Since the ElementTree module does not support CDATA. We're 
# extending the ElementTree Class to support generating XML with
# CDATA elements.


def CDATA(text=None):
    """
    A CDATA element factory function that uses the function itself as the tag
    (based on the Comment factory function in the ElementTree implementation).
    """    
    element = Element('CDATA')
    element.text = text
    return element

# We're replacing the _write method of the ElementTree class so that it would 
# recognize and correctly print out CDATA sections.
original_element_tree = ElementTree
class ElementTree_CDATA(original_element_tree):
    def _write(self, file, node, encoding, namespaces):
        if node.tag is 'CDATA':
            if node.text:
                text = node.text.encode(encoding)
                file.write("<![CDATA[%s]]>" % text)
        else:
            original_element_tree._write(self, file, node, encoding, namespaces)

#################################################################
class A2ATask(Task):

    def cb_start(self, method, uri, res, result, context, **kwargs):
        '''
        Implementation of the start method
        '''
        uri_a2a = context.uri('a2a:')
        context.process_all(uri_a2a.ws.collection(True), put='cb_generate_a2a_request_input')
        context.watch_all(['put'], uri_a2a.requests.collection(True), 'cb_issue_a2a_request')
        return (False, None)
            
    def cb_generate_a2a_request_input(self, method, uri, res, result, context, **kwargs):
        """
        Generate an a2a request
        
        Generates a a2a request resource and places it into spaces. It returns to the caller the URI at which the
        request has been placed.
        
        Return: datastore.uri.Uri
        """
        ws_endpoint = "https://{0}.landexchange.vic.gov.au:8444/spear_a2a/services/".format(res.environment)
        (ws_name, ws_operation) = uri.collection(False).split()[-2:]
        ds = DataStore(context.spaces.ns)
        uri_spear = context.uri('spear:')
        uri_datastore = context.uri('datastore:')
        uri_a2a = context.uri('a2a:')
        client_cert_resource = self.__get_client_cert(uri_datastore, uri_spear, ds)
        # get the wsdl resource
        resources = ds.find([(uri_datastore.resource.type, uri_spear.type.wsdl),
                 (uri_spear.wsdl.name, ws_name)])
        wsdl_resource = resources[0]
        # get the namespace dictionary
        element_dictionary = wsdl_resource.element_dictionary
        # get the input data to the webservice
        data = res.data
        # create soap request message
        env = ET.Element('{http://schemas.xmlsoap.org/soap/envelope/}Envelope')
        body = ET.SubElement(env, '{http://schemas.xmlsoap.org/soap/envelope/}Body')
        self.generate_soap_element(data, element_dictionary, body, '')  
        soap_message = '<?xml version="1.0" encoding="utf-8" ?>\r\n' + self.get_elements_as_string(env)
        
        res = Resource()
        res.ws_name = ws_name
        res.soap_message = soap_message
        res.client_cert_resource = client_cert_resource
        res.ws_endpoint = ws_endpoint + ws_name
        id = context.uuid()
        request_uri = uri_a2a.requests.add(str(id))
        context.put(request_uri, res)
        # return a copy of the uri as we dont want changes to the return uri by the caller to affect 
        # how we use the uri
        return (True, request_uri.uri_copy())
    
    def get_elements_as_string(self, root_element):
        """
        Return xml as string.
        """
        class dummy:
            pass
        data = []
        file = dummy()
        file.write = data.append
        # use our custom ElementTree_CDATA
        ElementTree_CDATA(root_element).write(file, None)
        return "".join(data)          
                            
    def generate_soap_element(self, element_data, element_dictionary, parent_element, path_to_parent_element):
        """
        Generate the soap elements of a soap message for a a2a request
        
        Generates the soap elements of a soap message. It traverses the 'element_data', and recursively calls itself to
        generate the soap element for each piece of element_data it comes across. The soap element is then added to the
        parent_element.
        
        Return: None
        
        Keyword arguments:
        element_data -- Dictionary: element_data to be converted to soap element. One key, value combination per dictionary.
        The key is the element's name, the value is either the text value of the element or child element(s). Child elements 
        will be represented as a List of Dicts or ResourceDicts.
        element_dictionary -- Dictionary: Dictionary with 'xpath like' path to a element as the key and the element's namespace 
        as the value
        parent_element -- xml.etree.elementtree.Element: parent element of the current element we are creating in the tree
        path_to_parent_element -- String: 'xpath like' path to the parent element 
        """
        
        for element_name in element_data.keys():
            # if the path_to_parent_element is not empty then add to it
            if path_to_parent_element:
                path_to_current_element = ".".join([path_to_parent_element, element_name])
            # if it is empty then just set the current element name as the path to the current element
            else:
                path_to_current_element = element_name
            if element_name == 'CDATA':
                cdata = CDATA(element_data[element_name])
                parent_element.append(cdata)
            else:
                # get the namespace associated with the current element
                element_def = element_dictionary[path_to_current_element]
                # create the element as a child of the parent element
                soap_element = ET.SubElement(parent_element, element_def.fully_qualified_name)
                # get the value of the element. The value can either be a string or a list of other dicts
                value = element_data[element_name]
                # if it is a list the generate soap elements for all the child elements
                if type(value) is types.ListType:
                    for child_element_data in value:
                        self.generate_soap_element(child_element_data, element_dictionary, soap_element, path_to_current_element)
                # if it is not a list, then assume the value is a string and set it as the text of the current element
                else:
                    if value:
                        soap_element.text = value
            
    def cb_issue_a2a_request(self, method, uri, new_uri, result, context, **kwargs):
        """
        Issue an a2a request.
        
        Watches the a2a request resources placed in spaces and issues the request to SPEAR. It then parses the response
        and places the response resource under the request uri, under the path 'response'. (E.g. If the request uri was 
        the uri http://a/b/c, then the response will be under http://a/b/c/response)
        
        Return: None 
        """        
        if uri.split2()[-1] == 'response':
            # just ignore responses
            return (False, None)
        
        # get the request resource
        request_resource = context.get(new_uri)
        uri_spear = context.uri('spear:')
        soap_message = request_resource.soap_message
        client_cert_resource = request_resource.client_cert_resource
        ws_endpoint = request_resource.ws_endpoint
        ctx = self.__get_ssl_context(client_cert_resource, uri_spear)
        try:
            # issue the request to SPEAR
            self.log(soap_message, logging.DEBUG)
            response = self.__issue_a2a_request(ctx, ws_endpoint, soap_message)
            result = self.parse_soap_response(response, context, request_resource.ws_name)
        except RequestError as error:
            # TODO: return some sort of exception to caller to indicate a error occured
            result = self.get_result_from_error(error)
        # put the response back into spaces
        context.put(uri.response, result)
        return (False, None)
    
    def parse_soap_response(self, soap_response, spaces_context, ws_name):
        """
        Return soap response as resource.
        
        The resource returned is the 'body' section of the soap response.
        The tag names (excluding namespace) are stored as attributes of
        the resource and the text of a tag is stored as its value. This
        makes navigating the response easier through the Resource
        API
        
        Return: datastore.resource.Resource 
        
        Keyword arguments:
        soap_response -- String: the xml soap response.
        spaces_context -- datastore.spaces.SpacesContext:
        ws_name -- String: name of the webservice.
        """
        result = Resource() 
        # indicates that the call was successful
        result.success = True

        ds = DataStore(spaces_context.spaces.ns)
        uri_spear = spaces_context.uri('spear:')
        uri_datastore = spaces_context.uri('datastore:')      
        # get the wsdl resource
        resources = ds.find([(uri_datastore.resource.type, uri_spear.type.wsdl),
                 (uri_spear.wsdl.name, ws_name)])
        wsdl_resource = resources[0]
      	# parse into element tree
        response = ET.XML(soap_response)

        # get to the soap body node
        soap_body = response.find('.//{http://schemas.xmlsoap.org/soap/envelope/}Body')
        # get the first child of the soap body
        ws_response = soap_body.getchildren()[0]
        # parse the first node
        result = self.parse_node(ws_response, result, spaces_context, ws_name, wsdl_resource, None)
        return result
    
    def parse_node(self, node, result, spaces_context, ws_name, wsdl_resource, path_to_parent_element):
        """
        Return node and its children parsed into a Resource
        
        The resource returned contains the node and the node's 
        children data
        
        Return: datastore.resource.Resource
        
        Keyword arguments:
        node -- xml.etree.elementtree.Element: Node to be parsed.
        result -- datastore.resource.Resource: Resource for the node data
        to be added to.
        spaces_context -- datastore.spaces.SpacesContext:
        ws_name -- String: name of the webservice.
        wsdl_resource -- datastore.resource.Resource: WSDL resource containing data extracted from wsdl file.
        path_to_parent_element -- String: string path to parent element.
        """  
                      
        # node.tag = {namespace}tagName. Hence split once from the right
        # at '}' and take the last element which will be the node name
        node_name = node.tag.rsplit('}', 1)[-1]
        # if there is no text in the node (remember to strip spaces, and new lines)
        if not node.text or not node.text.strip(' \r\n'):
            # if there is no text, then the attribute value will be a resource dictionary
            new_attrib = ResourceDict()
            # get path to current element
            if path_to_parent_element:
                element_path = path_to_parent_element + "." + node_name
            else:
                element_path = node_name 
            element_def = wsdl_resource.element_dictionary[element_path]
            # if the element is a array
            if element_def.array:
                try:
                    # try to get attribute
                    attrs = getattr(result, node_name)
                    attrs.append(new_attrib)
                    # set the list as the attribute
                    setattr(result, node_name, attrs)                    
                except AttributeError:
                    # create a list
                    attrs = [new_attrib]
                    setattr(result, node_name, attrs)
                except KeyError:
                    # create a list
                    attrs = [new_attrib]
                    setattr(result, node_name, attrs)
            # if it is not a array
            else:
                setattr(result, node_name, new_attrib)

            # handle the children
            if node.getchildren():
                for child_node in node.getchildren():
                    # parse the child node
                    self.parse_node(child_node, new_attrib, spaces_context, ws_name, wsdl_resource, element_path)
            # if there isnt any children, will set the attribute value to None
            # instead of the 'default' empty ResourceDict
            else:
                setattr(result, node_name, None)                
        # if there is a text value
        else:
            # set the node_name as the attribute name and the node text as the value
            setattr(result, node_name, node.text)
        
        return result    
    
    def get_result_from_error(self, error):
        """
        Return error result as Resource
        
        The resource returned contains the data from the error
        
        Return: datastore.resource.Resource
        
        Keyword arguments:
        error -- tuple: Tuple of the form (status, reason). Both are strings
        """
        
        (status, reason) = error
        result = Resource()
        # indicates the call was unsuccessful
        result.success = False
        result.status = status
        result.reason = reason
        return result
    
    def __issue_a2a_request(self, ctx, ws_endpoint, soap):
        res = self.__https_post(ctx, ws_endpoint, soap, followredirect=True)
        return res
            
    def __get_client_cert(self, uri_datastore, uri_spear, ds):
        client_cert = ds.find([(uri_datastore.resource.type, uri_spear.type.client_cert)])[0]
        return client_cert
    
    def __get_ssl_context(self, cert_resource, uri_spear):
        '''returns a context for SSL connections with the provided certificate loaded into the context '''
        cert_file_name = 'temp'
        ctx = SSL.Context('sslv23')
        temp = open(cert_file_name, 'wb')
        # resource content is the certificate
        temp.write(cert_resource.content)
        temp.close()
        temp = None
        # cert_file_name has to be a string which points to a certificate file
        ctx.load_cert(cert_file_name, callback=lambda x: cert_resource.get_single_metadata(uri_spear.cert.password))
        os.remove(cert_file_name)
        return ctx          
    
    def __https_post(self, ctx, uri, body, followredirect=False):
        """
        HTTP GET for a client certificate protected SSL encrypted resource.
        Returns None if there is an error.
        """
        resp = self.__https_req(ctx, 'POST', uri, followredirect, body)
        return resp.read()

    def __https_req(self, ctx, method, uri, followredirect=False, body=None):
        """
        HTTP request for a client certificate protected SSL encrypted resource.
        If followredirect=True, then follow redirection (302) forever.
        Returns the response object.
        """
        debug = 0

        parsed_uri = urlparse(uri)
        h = httpslib.HTTPSConnection(parsed_uri.hostname, parsed_uri.port, ssl_context=ctx)
        h.set_debuglevel(debug)
        h.putrequest(method, parsed_uri.path)
        #h.putheader('Accept', 'application/xml')
        #h.putheader('Accept', 'text/xml')
        h.putheader('SOAPAction', '')
        h.putheader('Connection', 'close')
        if body is not None:
            h.putheader('Content-Type', 'application/xml')
            h.putheader('Content-Length', len(body))
            h.endheaders()
            h.send(body)
        else:
            h.endheaders()
        resp = h.getresponse()
        if followredirect is not None:
            if resp.status == httplib.FOUND:
                redirect = resp.getheader('Location')
                print('Redirect to ({0}): {1}'.format(httplib.FOUND, redirect))
                return self.__https_req(ctx, method, redirect, followredirect)
        if resp.status == httplib.OK:
            return resp
        else:
            raise RequestError(resp.status, 'HTTP Error - ' + resp.reason)
              
class RequestError(Exception):
    """Base class for request related errors."""